package naga.x.game.template;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.FileInputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import com.alibaba.fastjson.JSON;

import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.hash.TIntObjectHashMap;
import naga.x.App;
import naga.x.game.template.annotation.Binding;
import naga.x.game.template.annotation.Excel;
import naga.x.util.EnumUtils;
import naga.x.util.IOUtils;
import naga.x.util.NumberUtils;
import naga.x.util.StringUtils;

public class TemplateParser {

	private Map<Class<? extends Template>, TIntObjectMap<? extends Template>> templates;
	private Map<Class<? extends Template>, List<? extends Template>> templatesList;
	
	private Map<String, Sheet> sheets = new HashMap<String, Sheet>(); // cache sheet
	
	public TemplateParser(TemplateService service) {
		this.templates = service.templates;
		this.templatesList = service.templatesList;
	}
	
	public void parse(Class<?> templateClass) throws TemplateException {
		try {
			parse(templateClass, templateClass.getDeclaredAnnotation(Excel.class));
		} catch (TemplateException e) {
			throw e;
		} catch (Exception e) {
			throw new TemplateException("parse error: " + templateClass, e);
		}
	}
	
	private void parse(Class<?> templateClass, Excel excel) throws Exception {
		if (excel.isStatic()) {
			parseStatic(templateClass, loadSheet(excel));
		} else if (!templatesList.containsKey(templateClass)) { // 有可能提前被当做依赖项加载过了
			parse((Class<? extends Template>) templateClass, loadSheet(excel));
		}
	}
	
	private Sheet loadSheet(Excel excel) throws TemplateException {
		return loadSheet(excel.value(), excel.sheetName(), excel.sheet());
	}
	
	private Sheet loadSheet(String xlsName, String sheetName, int sheetIndex) throws TemplateException {
		String sheetKey = xlsName + '-' + (StringUtils.isEmpty(sheetName) ? sheetIndex : sheetName);
		Sheet sheet = sheets.get(sheetKey);
		if (sheet != null) {
			return sheet;
		}
		Workbook workbook = null;
		try {
			File xls = new File(App.getApp().getConfig().dataDir, xlsName);
			if (xls.getName().endsWith(".xlsx")) {
				workbook = new XSSFWorkbook(xls);
			} else {
				workbook = new HSSFWorkbook(new FileInputStream(xls));
			}
			for (int i = workbook.getNumberOfSheets() - 1; i >= 0; i--) {
				Sheet s = workbook.getSheetAt(i);
				sheets.put(xlsName + '-' + i, s); // index
				sheets.put(xlsName + '-' + s.getSheetName(), s); // name
			}
			sheet = sheets.get(sheetKey);
		} catch (Exception e) {
			throw new TemplateException(e);
		} finally {
			IOUtils.closeQuietly(workbook);
		}
		if (sheet == null) {
			throw new TemplateException("Sheet " + sheetIndex + "(" + sheetName + ")" + " not found in " + xlsName);
		}
		return sheet;
	}
	
	private <T extends Template> void parse(Class<T> templateClass, Sheet sheet) throws TemplateException {
		// parse head
		Row head = sheet.getRow(0);
		int from = head.getFirstCellNum();
		int to = head.getLastCellNum();
		List<String> headNames = new ArrayList<String>();
		for (int col = from; col < to; col++) {
			Cell cell = head.getCell(col);
			String val;
			if (cell == null || StringUtils.isEmpty(val = cell.getStringCellValue())) {
				break;
			}
			headNames.add(val.trim());
		}
		int headLen = headNames.size();
		
		// parse data
		int lastRowNum = sheet.getLastRowNum();
		for (int i = 3; i <= lastRowNum; i++) {
			Row row = sheet.getRow(i);
			if (row == null) {
				break;
			}
			Map<String, List<Object>> rowVals = new LinkedHashMap<String, List<Object>>(headLen);
			for (int col = 0; col < headLen; col++) {
				Cell cell = row.getCell(col);
				Object val = getCellValue(cell);
				// 不知道当初为什么要加这个判断，现在注掉了 2016.11.09
//				if (col == 0 && val == null) { // 第一列值为空则直接放弃解析
//					return;
//				}
				List<Object> list = rowVals.get(headNames.get(col));
				if (list == null) {
					list = new ArrayList<Object>(2);
					rowVals.put(headNames.get(col), list);
				}
				list.add(val);
			}
			if (isRowEmpty(rowVals)) {
				continue;
			}
			T template = newTemplate(templateClass, rowVals);
			if (template != null) {
				template.validate();
				Class<? extends Template> clazz = template.ofType();
				List<T> list = (List<T>) templatesList.get(clazz);
				if (list == null) {
					list = new ArrayList<T>();
					templatesList.put(clazz, list);
				}
				list.add(template);
				
				if (template.hasId()) {
					TIntObjectMap<T> map = (TIntObjectMap<T>) templates.get(templateClass);
					if (map == null) {
						map = new TIntObjectHashMap<T>();
						templates.put(clazz, map);
					}
					map.put(template.getId(), template);
				}
			}
		}
	}
	
	private <T extends Template> T newTemplate(Class<T> templateClass, Map<String, List<Object>> rowVals) throws TemplateException {
		try {
			BeanInfo bean = Introspector.getBeanInfo(templateClass, Object.class);
			PropertyDescriptor[] prpts = bean.getPropertyDescriptors();
			T template = templateClass.newInstance();
			for (PropertyDescriptor p : prpts) {
				Method setter = p.getWriteMethod();
				Class<?> type = p.getPropertyType();
				if (setter == null || type == null) {
					continue;
				}
				if (Template.class.isAssignableFrom(type)
						|| (type.isArray() && Template.class.isAssignableFrom(type.getComponentType()))
						|| List.class.isAssignableFrom(type)) {
					Binding bind = setter.getDeclaredAnnotation(Binding.class);
					if (bind == null) {
						bind = getAnnotation(templateClass, p.getName(), Binding.class);
					}
					if (bind != null) {
						List<Object> v = rowVals.get(bind.field());
						if (v != null && v.size() > 0) {
							List<? extends Template> ref = findTemplate(bind.type(), bind.ref(), v.get(0));
							invokeSetter(template, setter, type, ref);
						}
						continue;
					}
				}
				List<Object> val = rowVals.get(p.getName());
				if (val == null || val.isEmpty()) {
					continue;
				}
				invokeSetter(template, setter, type, val);
			}
			return template;
		} catch (Exception e) {
			throw new TemplateException(e);
		}
	}
	
	private static void invokeSetter(Object object, Method setter, Class<?> parameterType, List<?> vals) throws Exception {
		vals = trimListTail(vals);
		if (parameterType.isArray()) {
			Class<?> componentType = parameterType.getComponentType();
			Object arr = Array.newInstance(componentType, vals.size());
			for (int i = 0; i < vals.size(); i++) {
				Array.set(arr, i, cast(vals.get(i), componentType));
			}
			setter.invoke(object, arr);
		} else if (List.class.isAssignableFrom(parameterType)) {
			setter.invoke(object, new ArrayList(vals));
		} else {
			setter.invoke(object, cast(vals.isEmpty() ? null : vals.get(0), parameterType));
		}
	}
	
	private <T extends Template> List<T> findTemplate(Class<T> templateClass, String fieldName, Object fieldValue) throws TemplateException {
		List<T> vals = (List<T>) templatesList.get(templateClass);
		if (vals == null) {
			parse(templateClass);
			vals = (List<T>) templatesList.get(templateClass);
			if (vals == null) {
				return null;
			}
		}
		try {
			Field f = getField(templateClass, fieldName);
			f.setAccessible(true);
			fieldValue = cast(fieldValue, f.getType());
			
			List<T> founds = new ArrayList<T>();
			for (T val : vals) {
				Object fValue = f.get(val);
				if (fieldValue.equals(fValue)) {
					founds.add(val);
				}
			}
			return founds;
		} catch (Exception e) {
			throw new TemplateException(e);
		}
	}
	
	static <T extends Annotation> T getAnnotation(Class<?> clazz, String fieldName, Class<T> anno) {
		Field f = getField(clazz, fieldName);
		if (f == null) {
			return null;
		}
		return f.getDeclaredAnnotation(anno);
	}
	
	static Field getField(Class<?> clazz, String fieldName) {
		Field f = null;
		try {
			f = clazz.getDeclaredField(fieldName);
		} catch (Exception e) {
		}
		if (f == null) {
			Class<?> parent = clazz.getSuperclass();
			if (parent != null) {
				return getField(parent, fieldName);
			}
		}
		return f;
	}
	
	static void setFieldValue(Field field, Object object, Object value) {
		try {
			field.setAccessible(true);
			field.set(object, cast(value, field.getType()));
		} catch (Exception e) {
		}
	}
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	private static Object cast(Object src, Class<?> targetType) {
		if (src != null && targetType.isAssignableFrom(src.getClass())) {
			return src;
		}
		if (targetType == String.class) {
			return src != null ? src.toString() : null;
		}
		if (targetType == byte.class || targetType == Byte.class) {
			return NumberUtils.parseByte(src);
		}
		if (targetType == boolean.class || targetType == Boolean.class) {
			return NumberUtils.parseBoolean(src);
		}
		if (targetType == short.class || targetType == Short.class) {
			return NumberUtils.parseShort(src);
		}
		if (targetType == int.class || targetType == Integer.class) {
			return NumberUtils.parseInt(src);
		}
		if (targetType == long.class || targetType == Long.class) {
			return NumberUtils.parseLong(src);
		}
		if (targetType == float.class || targetType == Float.class) {
			return NumberUtils.parseFloat(src);
		}
		if (targetType == double.class || targetType == Double.class) {
			return NumberUtils.parseDouble(src);
		}
		if (targetType == char.class || targetType == Character.class) {
			return NumberUtils.parseChar(src);
		}
		if (src != null && targetType.isEnum()) {
			int ordinal = NumberUtils.parseInt(src, -1);
			if (ordinal != -1) {
				return EnumUtils.forOrdinal(ordinal, (Class<? extends Enum>) targetType);
			}
			return Enum.valueOf((Class<? extends Enum>) targetType, src.toString());
		}
		if (src != null) { // 尝试json解析
			try {
				return JSON.parseObject(src.toString(), targetType);
			} catch (Exception e) {}
		}
		return src;
	}
	
	private static List<?> trimListTail(List<?> list) {
		if (list == null) {
			return Collections.emptyList();
		}
		int fromIndex = 0;
		int toIndex = list.size();
//		while (fromIndex < toIndex && list.get(fromIndex) == null) {
//			fromIndex++;
//		}
		while (fromIndex < toIndex && list.get(toIndex - 1) == null) {
			toIndex--;
		}
		return fromIndex > 0 || toIndex < list.size() ? list.subList(fromIndex, toIndex) : list;
	}
	
	private static boolean isRowEmpty(Map<String, List<Object>> rowVals) {
		if (rowVals == null || rowVals.isEmpty()) {
			return true;
		}
		Set<Entry<String, List<Object>>> set = rowVals.entrySet();
		for (Entry<String, List<Object>> e : set) {
			List<Object> val = (List<Object>) trimListTail(e.getValue());
			e.setValue(val);
			if (!val.isEmpty()) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * 解析静态配置文件
	 * @param templateClass
	 * @param sheet
	 * @throws TemplateException
	 */
	private void parseStatic(Class<?> templateClass, Sheet sheet) throws TemplateException {
		// parse head
		Row head = sheet.getRow(0);
		int from = head.getFirstCellNum();
		int to = head.getLastCellNum();
		int nameCol = -1; // field name column index
		int valueCol = -1; // value column index
		int bindingCol = -1; // binding to other excel
		for (int col = from; col < to; col++) {
			Cell cell = head.getCell(col);
			String val;
			if (cell == null || StringUtils.isEmpty(val = cell.getStringCellValue())) {
				break;
			}
			val = val.trim();
			if ("name".equals(val)) {
				nameCol = col;
			} else if ("value".equals(val)) {
				valueCol = col;
			} else if ("binding".equals(val)) {
				bindingCol = col;
			}
			if (nameCol >= 0 && valueCol >= 0 && bindingCol >= 0) {
				break;
			}
		}
		if (nameCol < 0 || valueCol < 0) {
			throw new TemplateException("the static template has error, not found the name or value: " + templateClass + "->" + nameCol + ", " + valueCol);
		}
		
		// parse data
		int lastRowNum = sheet.getLastRowNum();
		for (int i = 1; i <= lastRowNum; i++) {
			Row row = sheet.getRow(i);
			if (row == null) {
				break;
			}
			// parse the filed name
			Cell cell = row.getCell(nameCol);
			Object value = getCellValue(cell);
			if (value == null) {
				continue;
			}
			String fieldName = String.valueOf(value);
			// parse the value
			cell = row.getCell(valueCol);
			value = getCellValue(cell);
			if (bindingCol != -1) {
				// parse the value from binding excel
				cell = row.getCell(bindingCol);
				if (cell != null) {
					Object bindingVal = parseBindingValue(cell.getStringCellValue());
					if (bindingVal != null) value = bindingVal;
				}
			}
			if (value != null) {
				Field field = getField(templateClass, fieldName);
//				if (field == null) {
//					throw new TemplateException("the static template has error, not found the field: " + templateClass + "->" + fieldName);
//				}
				if (field != null) {
					setFieldValue(field, null, value);
				}
			}
		}
	}
	
	private static Object getCellValue(Cell cell) {
		if (cell == null) {
			return null;
		}
		int type = cell.getCellType();
		if (type == Cell.CELL_TYPE_STRING) {
			String v = cell.getStringCellValue();
			return StringUtils.isEmpty(v) ? null : v;
		}
		if (type == Cell.CELL_TYPE_NUMERIC || type == Cell.CELL_TYPE_FORMULA) {
			return cell.getNumericCellValue();
		}
		if (type == Cell.CELL_TYPE_BOOLEAN) {
			return cell.getBooleanCellValue();
		}
		// Cell.CELL_TYPE_BLANK
		// Cell.CELL_TYPE_ERROR
		return null;
	}
	
	private static int parseColNum(String colTag) {
		int n = 0;
		for (int i = 0; i < colTag.length(); i++) {
			n *= 26;
			n += colTag.charAt(i) - 'A' + 1;
		}
		return n;
	}
	
	private Object parseBindingValue(String binding) throws TemplateException {
		if (StringUtils.isEmpty(binding)) {
			return null;
		}
		String[] strs = StringUtils.split(binding, "-"); // xlsName-sheet-row-col
		String xlsName = strs[0];
		int sheetIndex = NumberUtils.parseInt(strs[1]);
		int row = NumberUtils.parseInt(strs[2]) - 1; // 1~
		int col = parseColNum(strs[3]) - 1; // A~
		Sheet sheet = loadSheet(xlsName, null, sheetIndex);
		return getCellValue(sheet.getRow(row).getCell(col));
	}
	
}
